package com.xh.baselibrary.ex

import java.io.*
import java.nio.charset.Charset

/**
 * Note:
 *
 * @author sens 2019/11/15 14:43
 */


/**
 * Gets the entire content of this file as a String using UTF-8 or specified [charset].
 *
 * This method is not recommended on huge files. It has an internal limitation of 2 GB file size.
 *
 * @param ignoreException whether to ignore exception information. If true, an empty string will
 *                        be returned once an exception occurs.
 * @param charset         character set to use.
 * @return the entire content of this file as a String.
 */
fun File.readText(ignoreException: Boolean, charset: Charset = Charsets.UTF_8): String {
    val result = this.runCatching { this.readText(charset) }
    return result.getOrElse { if (ignoreException) "" else throw it }
}

/**
 * Gets the entire content of this file as a byte array.
 *
 * This method is not recommended on huge files. It has an internal limitation of 2 GB byte array size.
 *
 * @param ignoreException whether to ignore exception information. If true, an empty byte array will
 *                        be returned once an exception occurs.
 *
 * @return the entire content of this file as a byte array.
 */
fun File.readBytes(ignoreException: Boolean): ByteArray {
    val result = this.runCatching { this.readBytes() }
    return result.getOrElse { if (ignoreException) byteArrayOf() else throw it }
}

/**
 * Gets the entire content of this input stream as a String using UTF-8 or specified [charset].
 *
 * This method is not recommended on huge content input stream. It has an internal limitation of 2 GB size.
 *
 * **Note**: It is the caller's responsibility to close this stream.
 *
 * @param ignoreException whether to ignore exception information. If true, an empty string will
 *                        be returned once an exception occurs.
 * @param charset         character set to use. By default uses UTF-8 charset.
 * @return the entire content of this input stream as a String.
 */
fun InputStream.readText(ignoreException: Boolean, charset: Charset = Charsets.UTF_8): String {
    val result = this.runCatching { this.readBytes().toString(charset) }
    return result.getOrElse { if (ignoreException) "" else throw it }
}

/**
 * Reads this stream completely into a byte array.
 *
 * **Note**: It is the caller's responsibility to close this stream.
 *
 * @param ignoreException whether to ignore exception information. If true, an empty byte array will
 *                        be returned once an exception occurs.
 */
fun InputStream.readBytes(ignoreException: Boolean): ByteArray {
    val result = this.runCatching { this.readBytes() }
    return result.getOrElse { if (ignoreException) byteArrayOf() else throw it }
}

/**
 * Reads this reader completely as a String.
 *
 * *Note*:  It is the caller's responsibility to close this reader.
 *
 * @param ignoreException whether to ignore exception information. If true, an empty string will
 *                        be returned once an exception occurs.
 * @return the string with corresponding file content.
 */
fun Reader.readText(ignoreException: Boolean): String {
    val result = this.runCatching { this.readText() }
    return result.getOrElse { if (ignoreException) "" else throw it }
}

/**
 * Gets the entire content of this reader as a CharArray.
 *
 * This method is not recommended on huge content reader. It has an internal limitation of 2 GB size.
 *
 * **Note**: It is the caller's responsibility to close this reader.
 *
 * @param ignoreException whether to ignore exception information. If true, an empty char array will
 *                        be returned once an exception occurs.
 * @return the entire content of this input stream as a CharArray.
 */
fun Reader.readChars(ignoreException: Boolean): CharArray {
    val result = runCatching {
        val list = mutableListOf<Char>()
        val buffer = CharArray(DEFAULT_BUFFER_SIZE)
        var chars = this.read(buffer)
        while (chars >= 0) {
            if (chars == buffer.size) {
                list.addAll(buffer.toList())
            } else {
                list.addAll(buffer.toList().subList(0, chars))
            }
            chars = this.read(buffer)
        }
        list.toTypedArray().toCharArray()
    }

    return result.getOrElse { if (ignoreException) charArrayOf() else throw it }
}

/**
 * Reads the file content as a list of lines.
 *
 * Do not use this function for huge files.
 *
 * @param ignoreException whether to ignore exception information. If true, an empty string list will
 *                        be returned once an exception occurs.
 * @param charset character set to use. By default uses UTF-8 charset.
 * @return list of file lines.
 */
fun File.readLines(ignoreException: Boolean, charset: Charset = Charsets.UTF_8): List<String> {
    val result = this.runCatching { this.readLines(charset) }
    return result.getOrElse { if (ignoreException) mutableListOf() else throw it }
}

/**
 * Reads the reader content as a list of lines.
 *
 * Do not use this function for huge files.
 *
 * **Note**: It is the caller's responsibility to close this reader.
 *
 * @param ignoreException whether to ignore exception information. If true, an empty string list will
 *                        be returned once an exception occurs.
 * @return list of file lines.
 */
fun Reader.readLines(ignoreException: Boolean): List<String> {
    val result = runCatching { this.readLines() }
    return result.getOrElse { if (ignoreException) listOf() else throw it }
}

/**
 * Iterates through each line of this reader, calls [action] for each line read
 * and closes the [Reader] when it's completed.
 *
 * @param ignoreException whether to ignore exception information.
 * @param action          function to process file lines.
 */
fun Reader.forEachLine(ignoreException: Boolean, action: (String) -> Unit) {
    val result = runCatching { this.forEachLine(action) }
    if (!ignoreException) result.exceptionOrNull()
}

/**
 * Sets the content of this file as [text] encoded using UTF-8 or specified [charset].
 * If this file exists, it becomes overwritten.
 *
 * @param text            text to write into file.
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @param charset         character set to use.
 * @return Return true when write successfully, otherwise return false.
 */
fun File.write(text: String, ignoreException: Boolean, charset: Charset = Charsets.UTF_8): Boolean {
    val result = runCatching {
        this.writeText(text, charset)
        true
    }
    return result.getOrElse { if (ignoreException) false else throw it }
}

/**
 * Sets the content of this file as an [array] of bytes.
 * If this file already exists, it becomes overwritten.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @param array           byte array to write into this file.
 * @return Return true when write successfully, otherwise return false.
 */
fun File.write(array: ByteArray, ignoreException: Boolean): Boolean {
    val result = runCatching {
        this.writeBytes(array)
        true
    }
    return result.getOrElse { if (ignoreException) false else throw it }
}

/**
 * Appends [text] to the content of this file using UTF-8 or the specified [charset].
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @param text            text to append to file.
 * @param charset         character set to use.
 * @return Return true when append successfully, otherwise return false.
 */
fun File.append(text: String, ignoreException: Boolean, charset: Charset = Charsets.UTF_8): Boolean {
    val result = runCatching {
        this.appendText(text, charset)
        true
    }
    return result.getOrElse { if (ignoreException) false else throw it }
}

/**
 * Appends an [array] of bytes to the content of this file.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @param array           byte array to append to this file.
 * @return Return true when append successfully, otherwise return false.
 */
fun File.append(array: ByteArray, ignoreException: Boolean): Boolean {
    val result = this.runCatching {
        this.appendBytes(array)
        true
    }
    return result.getOrElse { if (ignoreException) false else throw it }
}

/**
 * Sets the content of this path file as the content of this input stream.
 * If this path file already exists, it becomes overwritten.
 *
 * **Note**: It is the caller's responsibility to close this stream.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @return Return true when write successfully, otherwise return false.
 */
fun InputStream.writeTo(path: String, ignoreException: Boolean): Boolean
        = writeTo(this, File(path), false, ignoreException)

/**
 * Sets the content of this file as the content of this input stream.
 * If this file already exists, it becomes overwritten.
 *
 * **Note**: It is the caller's responsibility to close this stream.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @return Return true when write successfully, otherwise return false.
 */
fun InputStream.writeTo(file: File, ignoreException: Boolean): Boolean
        = writeTo(this, file, false, ignoreException)

/**
 * Use the content of this input stream appends to the path file.
 *
 * **Note**: It is the caller's responsibility to close this stream.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @return Return true when append successfully, otherwise return false.
 */
fun InputStream.appendTo(path: String, ignoreException: Boolean): Boolean
        = writeTo(this, File(path), true, ignoreException)

/**
 * Use the content of this input stream appends to the file.
 *
 * **Note**: It is the caller's responsibility to close this stream.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @return Return true when append successfully, otherwise return false.
 */
fun InputStream.appendTo(file: File, ignoreException: Boolean): Boolean
        = writeTo(this, file, true, ignoreException)

private fun writeTo(stream: InputStream, file: File, append: Boolean, ignoreException: Boolean): Boolean {
    if (file.exists() && !file.delete()) {
        return false
    }

    val result = runCatching {
        file.parentFile?.mkdirs()
        FileOutputStream(file, append).use {
            stream.forEach { buffer, size ->
                it.write(buffer, 0, size)
            }

            it.flush()
            it.fd.sync()
        }
        true
    }
    return result.getOrElse { if (ignoreException) false else throw it }
}

/**
 * Sets the content of this file as the content of this input stream.
 * If this file already exists, it becomes overwritten.
 *
 * **Note**: It is the caller's responsibility to close this stream.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @return Return true when write successfully, otherwise return false.
 */
fun InputStream.writeTo(out: OutputStream, ignoreException: Boolean): Boolean {
    val result = runCatching {
        this.forEach { buffer, size ->
            out.write(buffer, 0, size)
        }

        out.flush()
        true
    }
    return result.getOrElse { if (ignoreException) false else throw it }
}


/**
 * Sets the content of this path file as the content of this reader.
 * If this path file already exists, it becomes overwritten.
 *
 * **Note**: It is the caller's responsibility to close this reader.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @return Return true when write successfully, otherwise return false.
 */
fun Reader.writeTo(path: String, ignoreException: Boolean): Boolean
        = writeTo(this, File(path), false, ignoreException)

/**
 * Sets the content of this file as the content of this reader.
 * If this file already exists, it becomes overwritten.
 *
 * **Note**: It is the caller's responsibility to close this reader.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @return Return true when write successfully, otherwise return false.
 */
fun Reader.writeTo(file: File, ignoreException: Boolean): Boolean
        = writeTo(this, file, false, ignoreException)

/**
 * Use the content of this reader appends to the path file.
 *
 * **Note**: It is the caller's responsibility to close this reader.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @return Return true when append successfully, otherwise return false.
 */
fun Reader.appendTo(path: String, ignoreException: Boolean): Boolean
        = writeTo(this, File(path), true, ignoreException)

/**
 * Use the content of this reader appends to the file.
 *
 * **Note**: It is the caller's responsibility to close this reader.
 *
 * @param ignoreException whether to ignore exception information. If true, false will
 *                        be returned once an exception occurs.
 * @return Return true when append successfully, otherwise return false.
 */
fun Reader.appendTo(file: File, ignoreException: Boolean): Boolean
        = writeTo(this, file, true, ignoreException)

private fun writeTo(reader: Reader, file: File, append: Boolean, ignoreException: Boolean): Boolean {
    if (file.exists() && !file.delete()) {
        return false
    }

    val result = runCatching {
        file.parentFile?.mkdirs()
        FileWriter(file, append).use {
            reader.forEach { buffer, size ->
                it.write(buffer, 0, size)
            }

            it.flush()
        }
        true
    }
    return result.getOrElse { if (ignoreException) false else throw it }
}

/**
 * Rename or move files.
 *
 * @param overwrite overwrite when the target file exists. If the target file exists and does not overwrite,
 *                  false will be returned.
 * @return Return true when rename successfully, otherwise return false.
 */
fun File.renameTo(dstFile: File, overwrite: Boolean): Boolean {
    if (dstFile.exists()) {
        if (!overwrite || !dstFile.delete(true)) {
            return false
        }
    }

    return this.renameTo(dstFile)
}

/**
 * Delete file or entire directory
 *
 * @return Return true when all files are deleted successfully, otherwise return false.
 */
fun File.delete(deleteChild: Boolean): Boolean {
    if (!this.isDirectory) {
        return this.delete()
    }

    if (deleteChild) {
        val childFiles = this.listFiles()
        if (childFiles != null && childFiles.isNotEmpty()) {
            for (childFile in childFiles) {
                if (!childFile.delete(deleteChild)) {
                    return false
                }
            }
        }
    }

    return this.delete()
}
